Skip to content

feat(desktop): owner-global observer ingestion at app level#1493

Merged
tellaho merged 3 commits into
tho/active-turn-bridge-fixfrom
tho/observer-owner-global
Jul 7, 2026
Merged

feat(desktop): owner-global observer ingestion at app level#1493
tellaho merged 3 commits into
tho/active-turn-bridge-fixfrom
tho/observer-owner-global

Conversation

@tellaho

@tellaho tellaho commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #1492 (tho/active-turn-bridge-fix) — merge that first; this PR's base is set to it so the diff stays scoped.

Overview

Category: improvement
User Impact: Agent activity and working indicators (sidebar badges, profile badges, activity panels) now update reliably no matter which screen is open — including for remote agents you own but don't run locally.

Problem: Observer ingestion was mount-timing incidental: frames were only received, decrypted, and folded into derived liveness state while some surface happened to mount its own bridge with the right agent list. That made indicators brittle and inconsistent — a panel could show raw turn events while another surface's liveness stayed stale (the bug class fixed point-wise in #1492), and remote owned agents were only registered when specific panels were open.

Solution: Ingestion becomes a single app-level concern. A new useAgentObserverIngestion hook mounts once in AppShell, registers all locally managed agents plus relay agents the current identity declared-owns (NIP-OA ownerPubkey, resolved via one batched profile query), and feeds both the observer store and the derived active-turns store app-wide. All five per-surface bridge mounts are removed; surfaces now only read from the stores. This establishes the product invariant: if you own an agent, its turn activity is ingested app-wide.

File changes

desktop/src/features/agents/useAgentObserverIngestion.ts
New app-level ingestion hook plus pure combineObserverIngestionAgents(): managed agents keep their real status; declared-owned relay agents not managed locally are added as deployed. Non-owned agents are excluded — their frames are #p-addressed to their owner and never arrive on our subscription anyway.

desktop/src/features/agents/useAgentObserverIngestion.test.mjs
Unit coverage for the combination logic: status passthrough, owned-relay inclusion, foreign/ownerless exclusion, managed/relay dedupe, case-insensitive matching, unresolved-identity fallback.

desktop/src/app/AppShell.tsx
Mounts the ingestion hook alongside the other always-on workspace managers.

desktop/src/features/channels/ui/ChannelScreen.tsx
Removes the per-screen observer/turns bridge mounts and the synthetic observerBridgeAgents memo (profile-panel agents are covered by app-level owned-relay registration).

desktop/src/features/profile/ui/UserProfilePanel.tsx
Removes the panel-local bridge mounts and both synthetic agent-list memos — the owned-relay "deployed" seeding it did is now the app-level rule.

desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts
Sidebar working-badge hook becomes read-only over the derived store.

desktop/src/features/agents/ui/useManagedAgentActions.ts
Agents-page hook becomes read-only over the derived store.

desktop/src/features/agents/usePreventSleep.ts
Drops its own bridge mount; keeps reading observer snapshots for activity tracking.

Reproduction Steps

  1. Run the desktop app with a local managed agent, stay on the home screen (do not open any channel or the Agents page).
  2. Mention the agent in a channel from another client/build so it starts a turn.
  3. The sidebar working badge for that channel appears and clears with the turn — previously this depended on a surface with a bridge being mounted at the right time.
  4. For a remote agent you own (declared ownerPubkey, not managed locally): open its Activity from a profile anywhere — frames decrypt without needing the specific panels that used to seed the known-agents set.
  5. Automated: cd desktop && pnpm test (1558/1558, 7 new).

No visual/markup changes — this is ingestion plumbing consolidation.

@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from f5edf42 to f9ffb01 Compare July 6, 2026 16:51
tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main.

Make agent observer ingestion (kind 24200 frame decryption + derived
active-turn liveness) a single app-level concern instead of per-surface
bridge mounts:

- useAgentObserverIngestion mounted once in AppShell registers all
  locally managed agents plus relay agents the identity declared-owns
  (NIP-OA ownerPubkey via one batched profile query, treated deployed)
- Pure combineObserverIngestionAgents() with unit coverage
- Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel,
  useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep

Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent
memo (still feeds the profile activity preview); only its bridge mounts
(activityBridgeAgents) are removed since app-level ingestion covers both
managed and declared-owned relay agents.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/observer-owner-global branch from c44c50b to a7c6b39 Compare July 6, 2026 16:51
@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from f9ffb01 to 22feb20 Compare July 6, 2026 17:09
tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main.

Make agent observer ingestion (kind 24200 frame decryption + derived
active-turn liveness) a single app-level concern instead of per-surface
bridge mounts:

- useAgentObserverIngestion mounted once in AppShell registers all
  locally managed agents plus relay agents the identity declared-owns
  (NIP-OA ownerPubkey via one batched profile query, treated deployed)
- Pure combineObserverIngestionAgents() with unit coverage
- Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel,
  useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep

Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent
memo (still feeds the profile activity preview); only its bridge mounts
(activityBridgeAgents) are removed since app-level ingestion covers both
managed and declared-owned relay agents.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/observer-owner-global branch from a7c6b39 to be5994c Compare July 6, 2026 17:09
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main.

Make agent observer ingestion (kind 24200 frame decryption + derived
active-turn liveness) a single app-level concern instead of per-surface
bridge mounts:

- useAgentObserverIngestion mounted once in AppShell registers all
  locally managed agents plus relay agents the identity declared-owns
  (NIP-OA ownerPubkey via one batched profile query, treated deployed)
- Pure combineObserverIngestionAgents() with unit coverage
- Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel,
  useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep

Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent
memo (still feeds the profile activity preview); only its bridge mounts
(activityBridgeAgents) are removed since app-level ingestion covers both
managed and declared-owned relay agents.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from 22feb20 to 8d0becf Compare July 6, 2026 18:08
@tellaho tellaho force-pushed the tho/observer-owner-global branch from be5994c to ec29b2e Compare July 6, 2026 18:08
@tellaho tellaho requested a review from wpfleger96 July 6, 2026 19:49
@tellaho tellaho marked this pull request as ready for review July 6, 2026 19:49
// Owner-global observer ingestion: receives + decrypts agent observer
// frames and keeps derived active-turn liveness in sync app-wide, so no
// individual screen/panel has to mount its own bridge for ingestion.
useAgentObserverIngestion();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [IMPORTANT · doc-clarity] useAgentObserverIngestion mounts before identity resolves — deferral is silent.

This is called unconditionally, without the startupReady/identity guard that the other identity-dependent hooks in this block use. When currentPubkey is undefined (pre-resolve), combineObserverIngestionAgents early-returns managed-agents-only, so relay-owned agents aren't included until a second render after identity resolves.

This is safe — the early-return handles it correctly, no data loss — but the deferral is invisible to a reader. Worth a one-line inline/jsdoc note that relay-owned inclusion is intentionally deferred until identity resolves, so nobody "fixes" it by adding a guard that breaks the managed-agent coverage during startup.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated by Ned 🤖 in b21837d.

const ownerByPubkey = new Map<string, string>();
for (const [pubkey, summary] of Object.entries(profiles ?? {})) {
if (summary.ownerPubkey) {
ownerByPubkey.set(normalizePubkey(pubkey), summary.ownerPubkey);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [IMPORTANT · doc-clarity] ownerByPubkey key is normalized but value is stored raw.

Here the key is normalizePubkey(pubkey) but the value is the raw summary.ownerPubkey. In combineObserverIngestionAgents the value is normalized at compare time (normalizePubkey(owner) === me), so this is correct end-to-end.

But the asymmetry (normalized key, raw value, normalize-at-compare) is a read trap, and the unit test only feeds pre-normalized 64-char lowercase keys — so the raw-value path is never exercised. Suggest either storing normalizePubkey(summary.ownerPubkey) as the value, or adding a test case with a mixed-case ownerPubkey to document the intent. Not a bug — a future footgun.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated by Ned 🤖 in b21837d.

…value

Two review follow-ups on owner-global observer ingestion:

- Document (AppShell + hook jsdoc) that useAgentObserverIngestion
  intentionally mounts before identity resolves: managed agents are
  ingested immediately, relay-owned agents join once currentPubkey
  arrives. Guards against a future identity/startup gate that would
  drop managed-agent coverage during startup.
- Store ownerByPubkey values normalized (was: normalized key, raw
  value, normalize-at-compare) so lookups and ownership comparisons
  never depend on relay casing. Behavior unchanged; removes the
  asymmetry footgun.

Addresses review feedback on #1493.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…1494)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
@tellaho tellaho merged commit a9908ac into tho/active-turn-bridge-fix Jul 7, 2026
18 checks passed
@tellaho tellaho deleted the tho/observer-owner-global branch July 7, 2026 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants